/*
 * Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
 */
package qunar.tc.decompiler.modules.decompiler.exps;

import qunar.tc.decompiler.main.DecompilerContext;
import qunar.tc.decompiler.main.collectors.BytecodeMappingTracer;
import qunar.tc.decompiler.main.collectors.CounterContainer;
import qunar.tc.decompiler.modules.decompiler.vars.CheckTypesResult;
import qunar.tc.decompiler.modules.decompiler.vars.VarVersionPair;
import qunar.tc.decompiler.struct.gen.VarType;
import qunar.tc.decompiler.struct.match.IMatchable;
import qunar.tc.decompiler.struct.match.MatchEngine;
import qunar.tc.decompiler.struct.match.MatchNode;
import qunar.tc.decompiler.struct.match.MatchNode.RuleValue;
import qunar.tc.decompiler.util.TextBuffer;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

public class Exprent implements IMatchable {
    public static final int MULTIPLE_USES = 1;
    public static final int SIDE_EFFECTS_FREE = 2;
    public static final int BOTH_FLAGS = 3;

    public static final int EXPRENT_ARRAY = 1;
    public static final int EXPRENT_ASSIGNMENT = 2;
    public static final int EXPRENT_CONST = 3;
    public static final int EXPRENT_EXIT = 4;
    public static final int EXPRENT_FIELD = 5;
    public static final int EXPRENT_FUNCTION = 6;
    public static final int EXPRENT_IF = 7;
    public static final int EXPRENT_INVOCATION = 8;
    public static final int EXPRENT_MONITOR = 9;
    public static final int EXPRENT_NEW = 10;
    public static final int EXPRENT_SWITCH = 11;
    public static final int EXPRENT_VAR = 12;
    public static final int EXPRENT_ANNOTATION = 13;
    public static final int EXPRENT_ASSERT = 14;

    public final int type;
    public final int id;
    public Set<Integer> bytecode = null;  // offsets of bytecode instructions decompiled to this exprent

    public Exprent(int type) {
        this.type = type;
        this.id = DecompilerContext.getCounterContainer().getCounterAndIncrement(CounterContainer.EXPRESSION_COUNTER);
    }

    public int getPrecedence() {
        return 0; // the highest precedence
    }

    public VarType getExprType() {
        return VarType.VARTYPE_VOID;
    }

    public int getExprentUse() {
        return 0;
    }

    public CheckTypesResult checkExprTypeBounds() {
        return null;
    }

    public boolean containsExprent(Exprent exprent) {
        if (equals(exprent)) {
            return true;
        }
        List<Exprent> lst = getAllExprents();
        for (int i = lst.size() - 1; i >= 0; i--) {
            if (lst.get(i).containsExprent(exprent)) {
                return true;
            }
        }
        return false;
    }

    public List<Exprent> getAllExprents(boolean recursive) {
        List<Exprent> lst = getAllExprents();
        if (recursive) {
            for (int i = lst.size() - 1; i >= 0; i--) {
                lst.addAll(lst.get(i).getAllExprents(true));
            }
        }
        return lst;
    }

    public Set<VarVersionPair> getAllVariables() {
        List<Exprent> lstAllExprents = getAllExprents(true);
        lstAllExprents.add(this);

        Set<VarVersionPair> set = new HashSet<>();
        for (Exprent expr : lstAllExprents) {
            if (expr.type == EXPRENT_VAR) {
                set.add(new VarVersionPair((VarExprent) expr));
            }
        }
        return set;
    }

    public List<Exprent> getAllExprents() {
        throw new RuntimeException("not implemented");
    }

    public Exprent copy() {
        throw new RuntimeException("not implemented");
    }

    public TextBuffer toJava(int indent, BytecodeMappingTracer tracer) {
        throw new RuntimeException("not implemented");
    }

    public void replaceExprent(Exprent oldExpr, Exprent newExpr) {
    }

    public void addBytecodeOffsets(Collection<Integer> bytecodeOffsets) {
        if (bytecodeOffsets != null && !bytecodeOffsets.isEmpty()) {
            if (bytecode == null) {
                bytecode = new HashSet<>(bytecodeOffsets);
            } else {
                bytecode.addAll(bytecodeOffsets);
            }
        }
    }

    // *****************************************************************************
    // IMatchable implementation
    // *****************************************************************************

    @Override
    public IMatchable findObject(MatchNode matchNode, int index) {
        if (matchNode.getType() != MatchNode.MATCHNODE_EXPRENT) {
            return null;
        }

        List<Exprent> lstAllExprents = getAllExprents();
        if (lstAllExprents == null || lstAllExprents.isEmpty()) {
            return null;
        }

        String position = (String) matchNode.getRuleValue(MatchProperties.EXPRENT_POSITION);
        if (position != null) {
            if (position.matches("-?\\d+")) {
                return lstAllExprents.get((lstAllExprents.size() + Integer.parseInt(position)) % lstAllExprents.size()); // care for negative positions
            }
        } else if (index < lstAllExprents.size()) { // use 'index' parameter
            return lstAllExprents.get(index);
        }

        return null;
    }

    @Override
    public boolean match(MatchNode matchNode, MatchEngine engine) {
        if (matchNode.getType() != MatchNode.MATCHNODE_EXPRENT) {
            return false;
        }

        for (Entry<MatchProperties, RuleValue> rule : matchNode.getRules().entrySet()) {
            MatchProperties key = rule.getKey();
            if (key == MatchProperties.EXPRENT_TYPE && this.type != (Integer) rule.getValue().value) {
                return false;
            }
            if (key == MatchProperties.EXPRENT_RET && !engine.checkAndSetVariableValue((String) rule.getValue().value, this)) {
                return false;
            }
        }

        return true;
    }

    @Override
    public String toString() {
        return toJava(0, BytecodeMappingTracer.DUMMY).toString();
    }
}